#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#

import os,os.path,sys
import struct,base64

from hashlib import md5 as MD5

from icewood import tinylog

from icewood.tca.filecell import FileCell
from icewood.tca.tyrantindex import TyrantIndex

class   Cabinet(object)  :

    def __init__(self, paths, index_config=None) :
        '''
        @param paths list of paths separated by ':'
        @param config dict of configure
        '''
        self._paths = paths.split(':')
        self._config = index_config

        self._bins = []
        self._bins_size = 0

        self._logger = tinylog.getLogger( self.__class__.__name__ )

        self.__setup()
        ## __init__()

    def __del__(self) :
        self.close()
        ## __del__()

    def close(self) :
        if self._bins :
            for _bin in self._bins :
                _bin.close()
            self._bins = None
        ## close()

    def flush(self) :
        if self._bins :
            for _bin in self._bins :
                _bin.flush()
        ## flush()

    def __setup(self) :
        # id of cabinet should come from some configuration, to keep consistence among processes
        for path in self._paths :
            bin = Bin(path, self._config)
            self._bins.append( bin )
            self._bins_size += 1
        ## __setup()

    ###################################################################

    def insertignore(self, key, data, timestamp=None) :
        '''insert or ignore

        @param timestamp if timestamp present, it should be considered as part of data
        '''
        k_index = self.__getKeyToIndex( key )
        bin = self.__getBin( k_index )

        return bin.insertignore(k_index, key, data, timestamp)
        ## insertignore

    def insert(self, key, data, timestamp=None) :
        '''insert or update

        @param timestamp if timestamp present, it should be considered as part of data
        '''
        k_index = self.__getKeyToIndex( key )
        bin = self.__getBin( k_index )

        return bin.insert(k_index, key, data, timestamp)
        ## insert()

    def insertappend(self, key, data, timestamp=None) :
        '''insert or append data to exist key

        @param timestamp if timestamp present, it should be considered as part of data
        '''
        k_index = self.__getKeyToIndex( key )
        bin = self.__getBin( k_index )

        return bin.insertappend(k_index, key, data, timestamp)
        ## insertappend()

    def find(self, key) :
        '''find and return all values binding to the key

        @param timestamp if timestamp present, it should be considered as part of data

        @return list-of-value
        '''
        k_index = self.__getKeyToIndex( key )
        bin = self.__getBin( k_index )

        return bin.find(k_index, key)
        ## find()

    def delete(self, key) :
        '''delete key and all data associate with it
        '''
        k_index = self.__getKeyToIndex( key )
        bin = self.__getBin( k_index )

        return bin.delete(k_index, key)
        ## delete()

    ###################################################################

    def __getBin(self, key_index) :
        if self._bins_size > 1 :
            key_n = struct.unpack('>H',key_index[13:15])[0]
        else :
            key_n = 0
        return self._bins[ key_n % self._bins_size ]
        ## __getCellToRead()

    def __getKeyToIndex(self, key) :
        return  MD5(key).digest()
        ## __getKeyToIndex()

    ## class Cabinet

#######################################################################


class   Bin(object)  :

    LOCATION_PACK_FORMAT = '>4sII'
    LOCATION_PACK_SIZE = struct.calcsize( LOCATION_PACK_FORMAT )
    LOCATION_NONE = struct.pack(LOCATION_PACK_FORMAT,'FFFF',0,0)
    LOCATION_NONE_base64 = base64.b64encode( LOCATION_NONE )

    INDEX_FILENAME = 'index.tcb'

    def __init__(self, path, index_config=None) :
        '''
        @param path directory where data and index files live
        @param config dict of configure
        '''
        self._config = index_config

        self._path = path

        self._logger = tinylog.getLogger( self.__class__.__name__ )

        self.__setup()
        ## __init__()

    def __del__(self) :
        self.close()
        ## __del__()

    def close(self) :
        if self._data_cell :
            self._data_cell.close()
            self._data_cell = None

        if self._indexer :
            self._indexer.close()
            self._indexer = None
        ## close()

    def flush(self) :
        if self._indexer :
            self._indexer.flush()
        ## flush()

    def __setup(self) :
        # id of cabinet should come from some configuration, to keep consistence among processes
        self._data_cell = FileCell( self._path )

        index_filename = os.path.join(self._path, self.INDEX_FILENAME)
        self._indexer = TyrantIndex(index_filename, self._config)
        ## __setup()

    ###################################################################

    def insertignore(self, k_index, key, data, timestamp=None) :
        '''insert or ignore

        @param timestamp if timestamp present, it should be considered as part of data
        '''
        prev_index_data = self._indexer.get( k_index )
        if prev_index_data :
            # found, just ignore
            return True

        cell = self.__getCellToWrite( k_index )
        fileno,offset = cell.getLocation()

        data_to_write = '\t'.join((key, self.LOCATION_NONE_base64, data))

        index_data = struct.pack( self.LOCATION_PACK_FORMAT, fileno, offset, len(data_to_write) )

        self._indexer.put( k_index, index_data )

        cell.write( data_to_write )

        return True
        ## insertignore

    def insert(self, k_index, key, data, timestamp=None) :
        '''insert or update

        @param timestamp if timestamp present, it should be considered as part of data
        '''
        cell = self.__getCellToWrite( k_index )
        fileno,offset = cell.getLocation()

        data_to_write = '\t'.join((key, self.LOCATION_NONE_base64, data))

        index_data = struct.pack( self.LOCATION_PACK_FORMAT, fileno, offset, len(data_to_write) )

        self._indexer.put( k_index, index_data )

        cell.write( data_to_write )

        return True
        ## insert()

    def insertappend(self, k_index, key, data, timestamp=None) :
        '''insert or append data to exist key

        @param timestamp if timestamp present, it should be considered as part of data
        '''
        prev_index_data = self._indexer.get( k_index )
        if prev_index_data :
            # find exist data, just append it
            prev_location = base64.b64encode( prev_index_data )
        else :
            # not found, create a new one
            prev_location = self.LOCATION_NONE_base64

        cell = self.__getCellToWrite( k_index )
        fileno,offset = cell.getLocation()

        data_to_write = '\t'.join((key, prev_location, data))

        index_data = struct.pack( self.LOCATION_PACK_FORMAT, fileno, offset, len(data_to_write) )

        self._indexer.put( k_index, index_data )

        cell.write( data_to_write )

        return True
        ## insertappend()

    def find(self, k_index, key, limit=20) :
        '''find and return all values binding to the key

        @param limit number of data to return

        @return list-of-value
        '''
        index_data = self._indexer.get( k_index )
        if not index_data :
            # not found, just return
            return None

        values = []
        for i in xrange(limit) :
            fileno,offset,length = struct.unpack(self.LOCATION_PACK_FORMAT, index_data)
            cell = self.__getCellToRead()
            data = cell.read( (fileno, offset) )
            if not data :
                self._logger.error('read nothing from cabinet (fileno:%s offset=%s length=%s)', fileno, offset, length)
                break
            _k,_prev_index,_v = data.split('\t',2)
            values.append( _v )
            if self.LOCATION_NONE_base64 == _prev_index :
                break
            index_data = base64.decode( _prev_index )

        return values
        ## find()

    def delete(self, k_index, key) :
        '''delete key and all data associate with it
        '''

        # TODO : to delete data associate with it

        # just delete index
        self._indexer.delete( k_index )

        ## delete()

    ###################################################################

    def __getCellToWrite(self, key) :
        return self._data_cell
        ## __getCellToWrite()

    def __getCellToRead(self) :
        return self._data_cell
        ## __getCellToRead()


    ## class Bin

